package org.jederik.studip.service.view;

import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import javax.ws.rs.*;
import javax.ws.rs.core.Response;

import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
import org.jederik.studip.service.Const.*;
import org.jederik.studip.service.exceptions.AuthenticationException;
import org.jederik.studip.service.exceptions.AuthorizationException;
import org.jederik.studip.service.exceptions.InternalServerErrorException;

import com.sun.jersey.api.NotFoundException;
import com.sun.jersey.impl.ApiMessages;

import au.com.bytecode.opencsv.CSVReader;
import au.com.bytecode.opencsv.CSVWriter;

/**
 * Implementation der Webservice-Methoden. Die Methoden, die eine Resource 
 * zurückliefern ({@code GET}, tun dies im CSV-Format. Hierbei ist die erste Zeile immer
 * die Überschriftenzeile. Andere Methoden können {@code true} oder {@code false}
 * zurückgeben, um über Erfolg/Misserfolg der Operation zu informieren.\n
 * Wenn bei der Bearbeitung des Request Fehler auftreten, so wird der entsprechende
 * HTTP Status Code mit einer Fehlermeldung zurückgesendet.
 * @author Jann-Frederik Laß
 *
 */
@Path("/")
@Produces("text/plain")
public class CSVEndpoint {

	// URL der XML-RPC-Schnittstelle von Stud.IP:
	private static String XML_RPC_URL = "http://localhost/studip/xmlrpc.php";

	// API-Key Für Stud.IP:
	private static String STUDIP_API_KEY = "jan#bachelor";

	// Client für den Verbindungsaufbau zu Stud.IP:
	private XmlRpcClient client = null;

	// Zugangsdaten des Nutzers werden aus den URL-Parametern gelesen:
	@QueryParam("username") private String username;
	@QueryParam("password") private String password;

	/**
	 * Standart Konstruktor. Die Werte {@code XML_RPC_URL} und 
	 * {@code STUDIP_API_KEY} müssen in der Datei {@code config.properties} 
	 * gesetzt sein.
	 */
	// Standart-Konstruktor:
	public CSVEndpoint() {

		// Konfigurationsdaten aus config.properties-Datei lesen:
		Properties config = new Properties();
		try {
			config.load(getClass().getResourceAsStream("/config.properties"));

			XML_RPC_URL = config.getProperty("XML_RPC_URL");
//			System.out.println("XML_RPC_URL = " + XML_RPC_URL);

			STUDIP_API_KEY = config.getProperty("STUDIP_API_KEY");
//			System.out.println("STUDIP_API_KEY = " + STUDIP_API_KEY);

		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	/*
	 * Hier wird ein XmlRpcClient erstellt und nach erfolgreicher 
	 * Authentifizierung des Nutzers den anderen Methoden zur Verfügung 
	 * gestellt.
	 */
	private void initClient() {

		// Vorkonfigurieren:
		XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();
		try {
			config.setServerURL(new URL(XML_RPC_URL));
		} catch (MalformedURLException e) {
			throw new WebApplicationException(
					e, Response.Status.INTERNAL_SERVER_ERROR);
		}

		// Neuen Client mit erstellter Konfiguration erzeugen:
		XmlRpcClient client = new XmlRpcClient();
		client.setConfig(config);

		// Authentizität überprüfen:
		authenticate(client);

		// Erst jetzt global zugänglich machen:
		this.client = client;
	}

	/*
	 * Hier wird der Nutzer authentifiziert.
	 */
	private void authenticate(XmlRpcClient client) {

		// Überprüfen, ob Zugangsdaten mitgeliefert wurden:
		if (username == null || password == null) 
			throw new AuthenticationException("Authentication required.");

		// Parameter vorbereiten:
		String[] params = new String[] {
				STUDIP_API_KEY,
				username,
				password
		};

		try {

			// Authentizität über RPC überprüfen lassen:
			String sid = (String) client.execute("check_credentials", params);

			// Überprüfen, ob gültige Session-ID zurückgeliefert wurde:
			if (sid == null || sid.length() != 32) {

				// Ansonsten Vorgang abbrechen:
				throw new AuthenticationException("Authentication failed.");
			}
		} catch (XmlRpcException e) {
			throw new InternalServerErrorException(e);
		}
	}

	/*
	 * Führt einen Remote Procedure Call durch.
	 */
	private Object executeRPC(String method, String ... params) {

		// Falls noch nicht geschehen, Client initialisieren:
		if (client == null) initClient();

		// RPC durchführen:
		Object result = null;
		try {
			result = client.execute(method, params);
		} catch (XmlRpcException e) {
			throw new InternalServerErrorException(e);
		}

		// Ergebnis zurückliefern:
		return result;

	}

	/*
	 * Stellt die Daten als CSV-Tabelle dar.
	 */
	@SuppressWarnings("unchecked")
	private String toCSV(String[] headers, Object[] structs) {

		// CSVWriter erzeugen:
		StringWriter sw = new StringWriter();
		CSVWriter out = new CSVWriter(sw);
		
		// Überschriftenzeile ausgeben:
		out.writeNext(headers);
		
		// Datenzeilen ausgeben:
		for (int i=0; i<structs.length; i++) {

			// Eintrag als Map casten:
			Map<String, Object> valueMap = (Map<String, Object>) structs[i];

			// Array erzeugen:
			String[] valueArray = new String[headers.length];

			// Felder in Array eintragen:
			for (int j=0; j<valueArray.length; j++) {
				valueArray[j] = (String) valueMap.get(headers[j]);
			}

			// Datensatz ausgeben:
			out.writeNext(valueArray);
		}

		// CSV-String zurückgeben:
		return sw.toString();
	}

	/**
	 * Liefert eine Zusammenfassung der den Nutzer betreffenden Änderungen seit 
	 * einem bestimmten Zeitpunkt.
	 * @param since Zeitpunkt in Sekunden (nicht Millisekunden!) nach Neujahr 1970
	 * @return Tabellenspalten:<p/>
	 * {@code type} Resourcen-Typ<p/>
	 * {@code count} Anzahl der geänderten Resourcen von diesem Typ
	 */
	@GET
	@Path(Notifications.PATH + "/{since}")
	public String getNotifications(@PathParam("since") String since) {
		
		// Jeder Nutzer is autorisiert, seine eigenen Benachrichtigungen 
		// abzufragen.
		
		// Resource(n) abrufen:
		Object[] result = (Object[]) executeRPC(
				"get_notifications", STUDIP_API_KEY, username, since);
		
		// Als CSV-Tabelle weiterleiten:
		return toCSV(Notifications.HEADERS, result);
	}

	/**
	 * Liefert alle den Nutzer betreffenden Änderungen seit einem Bestimmten 
	 * Zeitpunkt.
	 * @param since Zeitpunkt in Sekunden (nicht Millisekunden!) nach Neujahr 1970
	 * @return Tabellenspalten:<p/>
	 * {@code object_id} ID der geänderten Resource<p/>
	 * {@code object_type} Typ der geänderten Resource<p/>
	 * {@code title} Name der Resource<p/>
	 * {@code description} Beschreibung der Änderung<p/>
	 * {@code date} Zeitpunkt der Änderung<p/>
	 * {@code read} {@code 1} wenn die Resource seit der Änderung geöffnet wurde,
	 * {@code 0} sonst
	 */
	@GET
	@Path(Feed.PATH + "/{since}")
	public String getFeed(@PathParam("since") String since) {
		
		// Jeder Nutzer is autorisiert, seinen eigenen Feed abzufragen.
		
		// Resource(n) abrufen:
		Object[] result = (Object[]) executeRPC(
				"get_feed", STUDIP_API_KEY, username, since);
		
		// Als CSV-Tabelle weiterleiten:
		return toCSV(Feed.HEADERS, result);
	}

	/**
	 * Liefert einen bestimmten News-Artikel.
	 * @param id ID des Artikels
	 * @return Tabellenspalten:<p/>
	 * {@code topic} Überschrift<p/>
	 * {@code body} Nachrichten-Rumpf<p/>
	 * {@code auther} Verfasser<p/>
	 * {@code date} Datum der letzten Änderung<p/>
	 * {@code range} Bereich (Veranstaltung) auf den sich der Artikel bezieht
	 */
	@GET
	@Path(News.PATH + "/{id}")
	public String getNews(@PathParam("id") String id) {
		
		// Berechtigung überprüfen:
		boolean authoized = (Boolean) executeRPC(
				"has_news_authorization", 
				STUDIP_API_KEY, username, id);
		
		// Evtl. Status Code 403 zurückmelden:
		if (!authoized) 
			throw new AuthorizationException("You are not allowed to see this News!");
		
		// Resource(n) abrufen:
		Object[] result = (Object[]) executeRPC(
				"get_news", STUDIP_API_KEY, id, username);
		
		// Evtl. Status Code 404 zurückmelden:
		if (result.length == 0) 
			throw new NotFoundException("News could not be found.");
		
		// Als CSV-Tabelle weiterleiten:
		return toCSV(News.HEADERS, result);
	}

	/**
	 * Liefert eine bestimmte Nachricht.
	 * @param id ID der Nachricht
	 * @return Tabellenspalten:<p/>
	 * {@code subject} Betreff
	 * {@code date} Datum
	 * {@code author} Verfasser der Nachricht
	 * {@code text} Inhalt der Nachricht
	 * {@code author_uname} Username des Verfassers
	 */
	@GET
	@Path(Messages.PATH + "/{id}")
	public String getMessage(@PathParam("id") String id) {
		
		// Berechtigung überprüfen:
		boolean authoized = (Boolean) executeRPC(
				"has_message_authorization", 
				STUDIP_API_KEY, username, id);
		
		// Evtl. Status Code 403 zurückmelden:
		if (!authoized) 
			throw new AuthorizationException("You are not allowed to see this Message!");
		
		// Message abrufen:
		Object[] result = (Object[]) executeRPC(
				"get_message", STUDIP_API_KEY, id);
		
		// Evtl. Status Code 404 zurückmelden:
		if (result.length == 0) 
			throw new NotFoundException("Message could not be found.");
		
		// Als CSV-Tabelle weiterleiten:
		return toCSV(Messages.HEADERS, result);
	}
	
	private Map<String, Integer> getColumnIndizes(String[] columnNames) {
		
		Map<String, Integer> indizes = new HashMap<String, Integer>();
		
		for (int i=0; i<columnNames.length; i++) {
			indizes.put(columnNames[i], i);
		}
		
		return indizes;
		
	}

	/**
	 * Versendet eine Nachricht.
	 * @param in CSVDaten. Spaltennamen:<p/>
	 * {@code recipient} Nutzername des Empfänger<p/>
	 * {@code subject} Betreff<p/>
	 * {@code text} Inhalt der Nachricht
	 * @return {@code "true"} bei erfolgreichem Versenden, {@code "false"} bei
	 * Misserfolg
	 * @throws IOException Wenn Fehler beim Verarbeiten des CSV-Input auftreten
	 */
	@POST
	@Path(Messages.PATH)
	public void postMessage(Reader in) {
		
		// CSVReader erzeugen:
		CSVReader reader = new CSVReader(in);
		
		String[] headers;
		try {
			headers = reader.readNext();
		} catch (IOException e) {
			throw new InternalServerErrorException(e);
		}
		Map<String, Integer> indizes = getColumnIndizes(headers);
		final int IDX_RECIPIENT = indizes.get("recipient");
		final int IDX_SUBJECT = indizes.get("subject");
		final int IDX_TEXT = indizes.get("text");
		
		boolean success = true;
		
		// Datenzeilen durchlaufen:
		String[] values;
		try {
			while ((values = reader.readNext()) != null) {

				// Tabellenfelder auslesen:
				String recipient = values[IDX_RECIPIENT];
				String subject = values[IDX_SUBJECT];
				String text = values[IDX_TEXT];

				// send_message Methode per RPC ausführen:
				Object response = executeRPC(
						"send_message", 
						STUDIP_API_KEY, 
						username, 
						recipient, 
						subject, 
						text);

				// Erfolgsrückmeldung überprüfen:
				success = success && ((Boolean) response).booleanValue();
			}
		} catch (IOException e) {
			throw new InternalServerErrorException(e);
		}

		if (!success) throw new InternalServerErrorException("Message could not be sent!");
	}

	/**
	 * Liefert einen bestimmten Termin.
	 * @param id ID des Termins
	 * @return Tabellenspalten:<p/>
	 * {@code title} Titel des Termins<p/>
	 * {@code range} Bereich (Veranstaltung) zu dem der Termin gehört<p/>
	 * {@code start} Startzeit<p/>
	 * {@code end} Endzeit<p/>
	 * {@code description} Beschreibung
	 */
	@GET
	@Path(Schedules.PATH + "/{id}")
	public String getSchedule(@PathParam("id") String id) {
		
		// Berechtigung überprüfen:
		boolean authoized = (Boolean) executeRPC(
				"has_schedule_authorization", 
				STUDIP_API_KEY, username, id);
		
		// Evtl. Status Code 403 zurückmelden:
		if (!authoized) 
			throw new AuthorizationException("You are not allowed to see this Event!");
		
		// Resource(n) abrufen:
		Object[] result = (Object[]) executeRPC(
				"get_schedule", STUDIP_API_KEY, id);
		
		// Evtl. Status Code 404 zurückmelden:
		if (result.length == 0) 
			throw new NotFoundException("Schedule couldn't be found.");
		
		// Als CSV-Tabelle weiterleiten:
		return toCSV(Schedules.HEADERS, result);
	}

	/**
	 * Markiert eine Resource als gelesen.
	 * @param type Resourcentyp
	 * @param id ID der Resource
	 */
	@DELETE
	@Path(Objects.PATH + "/{type}/{id}")
	public void deleteObject(
			@PathParam("type") String type, 
			@PathParam("id") String id) {

		// RPC an set_object_visited-Methode durchführen:
		executeRPC("set_object_visited", STUDIP_API_KEY, id, type, username);
	}
	
	@GET
	@Path(MyCourses.PATH)
	public String getMycourses() {
		
		// Jeder Nutzer is autorisiert, seine eigenen Veranstaltungen 
		// abzufragen.
		
		// Resource(n) abrufen:
		Object[] result = (Object[]) executeRPC(
				"get_user_seminars", STUDIP_API_KEY, username);
		
		// Als CSV-Tabelle weiterleiten:
		return toCSV(MyCourses.HEADERS, result);
	}
	
	
	// TODO Tabellendetails:
	/**
	 * Liefert einen bestimmten Termin.
	 * @param id ID des Termins
	 * @return Tabellenspalten:<p/>
	 * {@code title} Titel des Termins<p/>
	 * {@code range} Bereich (Veranstaltung) zu dem der Termin gehört<p/>
	 * {@code start} Startzeit<p/>
	 * {@code end} Endzeit<p/>
	 * {@code description} Beschreibung
	 */
	@GET
	@Path(Courses.PATH + "/{id}" + Courses.Details.PATH)
	public String getCourseDetails(@PathParam("id") String id) {
		
		// Die Veranstaltungsdetails sind für jeden einsehbar.
		
		// Resource(n) abrufen:
		Object[] result = (Object[]) executeRPC(
				"get_seminar_details", STUDIP_API_KEY, id);
		
		// Evtl. Status Code 404 zurückmelden:
		if (result.length == 0) 
			throw new NotFoundException("Course couldn't be found.");
		
		// Als CSV-Tabelle weiterleiten:
		return toCSV(Courses.Details.HEADERS, result);
	}

	/**
	 * Liefert alle den Nutzer betreffenden Änderungen seit einem Bestimmten 
	 * Zeitpunkt.
	 * @param since Zeitpunkt in Sekunden (nicht Millisekunden!) nach Neujahr 1970
	 * @return Tabellenspalten:<p/>
	 * {@code object_id} ID der geänderten Resource<p/>
	 * {@code object_type} Typ der geänderten Resource<p/>
	 * {@code title} Name der Resource<p/>
	 * {@code description} Beschreibung der Änderung<p/>
	 * {@code date} Zeitpunkt der Änderung<p/>
	 * {@code read} {@code 1} wenn die Resource seit der Änderung geöffnet wurde,
	 * {@code 0} sonst
	 */
	@GET
	@Path(Courses.PATH + "/{id}" + Courses.Participants.PATH)
	public String getCourseParticipants(@PathParam("id") String id) {
		
		// Berechtigung überprüfen:
		boolean authoized = (Boolean) executeRPC(
				"has_seminar_authorization", 
				STUDIP_API_KEY, username, id);
		
		// Evtl. Status Code 403 zurückmelden:
		if (!authoized) 
			throw new AuthorizationException("You are not allowed to see this Courses Participants!");
		
		// Resource(n) abrufen:
		Object[] result = (Object[]) executeRPC(
				"get_seminar_participants", STUDIP_API_KEY, id);
		
		// Als CSV-Tabelle weiterleiten:
		return toCSV(Courses.Participants.HEADERS, result);
	}
	
	/**
	 * Liefert einen bestimmten Termin.
	 * @param id ID des Termins
	 * @return Tabellenspalten:<p/>
	 * {@code title} Titel des Termins<p/>
	 * {@code range} Bereich (Veranstaltung) zu dem der Termin gehört<p/>
	 * {@code start} Startzeit<p/>
	 * {@code end} Endzeit<p/>
	 * {@code description} Beschreibung
	 */
	@GET
	@Path(ExportSchedule.PATH + "/{since}")
	public String exportSchedule(@PathParam("since") String since) {
		
		// Resource(n) abrufen:
		Object[] result = (Object[]) executeRPC(
				"export_schedule", STUDIP_API_KEY, username, since);
		
		// Als CSV-Tabelle weiterleiten:
		return toCSV(ExportSchedule.HEADERS, result);
	}

	@GET
	@Path(VisitedObjects.PATH + "/{since}")
	public String getVisitedObjects(@PathParam("since") String since) {
		
		// Resource(n) abrufen:
		Object[] result = (Object[]) executeRPC(
				"get_visited_objects_since", STUDIP_API_KEY, username, since);
		
		// Als CSV-Tabelle weiterleiten:
		return toCSV(VisitedObjects.HEADERS, result);
	}


	
}















